<?php
/**
 * 使用方法：自动生成API文档【根据文档注释】
 * @desc \doc::$api->reflection(接口所在绝对路径, 接口命令空间【不包含接口名称】,关键词【搜索】(可选))
 * @ 1、不使用缓存
 * $doc = \doc::$api->setCache()->reflection(dirname(__DIR__).'/interfaces', 'app_supplier\modules\api\interfaces');
 * @ 2、使用缓存
 * $doc = \doc::$api->reflection(dirname(__DIR__).'/interfaces', 'app_supplier\modules\api\interfaces');
 * @ 3、使用缓存：缓存数据加密
 * doc = \doc::$api->setEncrypt()->reflection(dirname(__DIR__).'/interfaces', 'app_supplier\modules\api\interfaces');
 */

namespace  doc\analysis;

use phpDocumentor\Reflection\DocBlock\Tags\Var_;

class api
{
    /**
     * @var \doc\analysis\api
     * @name 根目录
     */
    private  $baseDir = null;

    /**
     * @var \doc\analysis\api
     * @name #文件目录
     */
    public $dir = null;

    /**
     * @var \doc\analysis\api
     * @name #文件上级目录
     */

    public $baseName = null;
    /**
     * @var \doc\analysis\api
     * @name #命令空间
     */
    private  $namSpcae = null;

    /**
     * @var \doc\analysis\api
     * @name 是否启用缓存
     */
    private static $cache = false;

    /**
     * @var \doc\analysis\api
     * #name 是否加密缓存文件
     */
    private static $encrypt = false;

    /**
     * @name 整个文档
     * @var array
     */
    public  $doc = [];

    /**
     * @name 菜单目录
     * @var array
     */
    public  $menu = [];

    /**
     * @name 当前选中的菜单
     * @var string
     */
    public  $selected = '';

    /**
     * @name 搜索关键词
     * @var string
     */
    public  $search = '';

    /**
     * @name 具体的内容
     * @var array
     */
    private static $info = [];
    /**
     * @doc 读取类【接口】的注释
     * @param $dir 类的目录
     * @param $namespace 类命令空间
     * @param null $search 搜索关键词
     * @return $this|array
     */
    public function reflection($dir, $namespace, $search=null, $selected='', $cahae = false)
    {
        $this->dir = rtrim($dir, '/');
        $this->namSpcae = rtrim($namespace, '\\');
        $this->selected = $selected;
        $this->search = $search;

        //判断文件存在不
        if (is_dir($this->dir)) {
            if ($path = pathinfo($this->dir)) {
                $this->baseDir = $path['dirname'];
                $this->baseName = $path['basename'];
            }
            $this->doc = !empty($search) ?  $this->search($search, $this->getFile()) : $this->getFile();
        }

        self::$cache =  $cahae;
        return $this;
    }

    /**
     * 获取菜单
     * @param $data
     * @param int $selected
     * @return array
     */
    public function menu($selected = '')
    {

        $menuStart[] = [
            "name" => "使用说明",
            "alias" => "help",
            "sort" => 0,
            "items" => [
                [
                    'title'=>'接口权限',
                    "name" => "help_auth",
                    "alias" => 'help-auth',
                    "sort" => 1,
                    'remark'=>'',
                    'selected'=>0
                ],
                [
                    'title'=>'接口返回',
                    "name" => "help_response",
                    "alias" => 'help-response',
                    "sort" => 2,
                    'remark'=>'',
                    'selected'=>''
                ]
            ]
        ];

        $alias = $selected;

        $data = array_values($this->doc);
        $menu = [];
        $selectFlag  =  false;#没有只添加一次
        foreach ($data as $k=>$v) {
            $list = [
                'name'=>$v['name'],
                'sort'=>$v['sort'],
                'date'=>$v['date'],
                'sign'=>$v['sign'],
                'md5'=>$v['md5'],
                //'deprecated'=>$v['deprecated'],
            ];
            if (isset($v['method']) && !empty($v['method'])) {
                $items = [];
                foreach ($v['method'] as $i=>$item) {
                    $items[] = [
                        'title'=>$item['title'],
                        'md5'=>$item['md5'],
                        'alias'=>$item['alias'],
                        'route'=>$item['route'],
                        'name'=>$item['name'],
                        'sign'=>$item['sign'],
                        'sort'=>$item['sort'],
                        'remark'=>$item['remark'],
                        'date'=>$item['date'],
                        'deprecated'=>$item['deprecated'],
                        'selected'=>$selected ? ($selected===$item['alias'] ? 1: 0) : 0
                    ];

                }
                $sortkey = $this->_array_column($items, 'sort');
                array_multisort($sortkey,SORT_ASC,  $items);
                if ( !$selectFlag && !$selected && isset($items[0]) && is_array($items[0])) {
                    $items[0]['selected'] = 1;
                    $alias = $items[0]['alias'];
                    $selectFlag = true;
                }

                $list['items'] = $items;
            }
            $menu[] = $list;
        }
        $sort = $this->_array_column($menu, 'sort');
        array_multisort($sort,SORT_ASC, $menu);
        $menuNew = array_merge($menuStart, $menu);
        $this->selected = $alias;

        foreach ($menuNew as &$m1) {

            foreach ($m1['items'] as &$m2) {
                $m2['flag'] = false;
                $m2['selected'] = 0;
                if ($this->selected == $m2['alias'])
                    $m2['selected'] = 1;
            }
        }

        $this->menu = $menuNew;
        return  $this->menu;
    }

    /**
     * 返回数组中指定的一列
     * @param $input            需要取出数组列的多维数组（或结果集）
     * @param $columnKey        需要返回值的列，它可以是索引数组的列索引，或者是关联数组的列的键。 也可以是NULL，此时将返回整个数组（配合index_key参数来重置数组键的时候，非常管用）
     * @param null $indexKey    作为返回数组的索引/键的列，它可以是该列的整数索引，或者字符串键值。
     * @return array            返回值
     */
    public function _array_column($input, $columnKey, $indexKey = null)
    {
        if (!function_exists('array_column')) {
            $columnKeyIsNumber = (is_numeric($columnKey)) ? true : false;
            $indexKeyIsNull = (is_null($indexKey)) ? true : false;
            $indexKeyIsNumber = (is_numeric($indexKey)) ? true : false;
            $result = array();
            foreach ((array)$input as $key => $row) {
                if ($columnKeyIsNumber) {
                    $tmp = array_slice($row, $columnKey, 1);
                    $tmp = (is_array($tmp) && !empty($tmp)) ? current($tmp) : null;
                } else {
                    $tmp = isset($row[$columnKey]) ? $row[$columnKey] : null;
                }
                if (!$indexKeyIsNull) {
                    if ($indexKeyIsNumber) {
                        $key = array_slice($row, $indexKey, 1);
                        $key = (is_array($key) && !empty($key)) ? current($key) : null;
                        $key = is_null($key) ? 0 : $key;
                    } else {
                        $key = isset($row[$indexKey]) ? $row[$indexKey] : 0;
                    }
                }
                $result[$key] = $tmp;
            }
            return $result;
        } else {
            return array_column($input, $columnKey, $indexKey);
        }
    }

    /**
     * 输出数据库：
     * @param $data
     * @param int $key
     * @return array
     */
    public function content($data)
    {
        if (!$data)
            return $data;
        $key = $this->selected;
        $content = [];
        foreach ($data as $k=>$argv) {
            foreach ($argv['method'] as $item) {
                if ($item['alias']===$key) {
                    $content = [
                        'title'=>$argv['name'].' - '.$item['title'],
                        'desc'=>$argv['desc'],
                        'alias'=>$argv['alias'],
                        'info'=>$item
                    ];
                    break 2;
                }
            }
        }

        self::$info = $content;

        return self::$info;
    }

    /**
     * 输出数据：
     * @return array
     */
    public function export()
    {
        $data = [
            'menu'=>$this->menu(!empty($this->selected) ? $this->selected : 0),
            'content'=>$this->content($this->doc, $this->selected),
            'doc'=>$this->doc,
            'search'=>$this->search,
        ];


        if ($this->getIsAjax()) {
            header('Content-type:application/json; charset=utf-8');
            echo json_encode($data, JSON_UNESCAPED_UNICODE);
            exit;
        } else {
            return $data;
        }

    }

    /**
     * 判断是不是ajax请求
     * @return bool
     */
    public function getIsAjax()
    {
        return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest';
    }

    /**
     * 设置缓存
     * @param bool $cache
     * @return $this
     */
    public function setCache($cache = true)
    {
        self::$cache = $cache;

        return $this;
    }

    /**
     * 设置缓存的加密数据
     * @param bool $encrypt
     * @return $this
     */
    public function setEncrypt($encrypt=true)
    {
        self::$encrypt = $encrypt;

        return $this;
    }

    /**
     * 获取文件
     * @return array
     */
    private function getFile()
    {
        $file = scandir($this->dir);
        $dirs = [];
        foreach ($file as $dir) {
            if ($dir != '.'&& $dir != '..') {
                $fileName = strpos($dir, '.php') ?  rtrim($dir, '.php') : $dir;
                $dirs[$fileName] = $this->getInterfaceInfo($dir);
            }
        }
        return $dirs;
    }

    /**
     * @param $key
     * @param $value
     * @return array
     */
    private  function search($key, $value)
    {


        $list = [];
        foreach ($value as $k=>$v) {
            $strArr = $this->_array_column($v['method'], 'title', 'md5');
            $method = [];

            foreach ($strArr as $i=>$item) {
                $line = strstr($item, $key);


                if ($line !== false) {
                    if (isset($v['method'][$i])) {
                        $method[$i] =  $v['method'][$i];
                    }
                }
            }
            if (!empty($method) && is_array($method)) {
                $list[$k] = $v;
                $list[$k]['method'] = $method;
            }
        }

        return $list;
    }

    /**
     * 获取文件【类】【接口】的详细信息
     * @param $fileName
     * @return array
     */
    private function getInterfaceInfo($fileName)
    {
        $interface = [];

        //接口全地址（物理地址）
        $filePath = strpos($fileName, '.php') === false ? $this->dir.'/'.$fileName.'.php' : $this->dir.'/'.$fileName;
        //命令空间+接口名称
        $namespace = strpos($fileName, '.php') === false ? $this->namSpcae.'\\'.$fileName : $this->namSpcae.'\\'.rtrim($fileName, '.php');
        if (!file_exists($filePath)) {
            return [];
        }
        //获取缓存文件
        $chaceKey = md5_file($filePath);
        //缓存+缓存文件是否要加密

        $cache = self::$cache ? (!self::$encrypt ? \doc::$cache->get($chaceKey) : \doc::$cache->setEncrypt()->get($chaceKey)) : false;

        if ($cache !== false) {
            return $cache;
        }

        require_once($filePath);
        //反射机制

        try {
            require_once($filePath);
            $reflections = new \ReflectionClass('\\'.$namespace);
        } catch (\Exception $e) {
            echo "不存{$filePath}";
        }

        $classDoc = $reflections->getDocComment();
        $alias = $reflections->isInterface() ? strtolower(substr(rtrim($fileName, '.php'),0,  -strlen('interface'))) : rtrim($fileName, '.php');
        $interface['md5'] = md5(sha1($classDoc));

        //获取模块
        preg_match("/modules\\\(?<m>[a-z]{1,})\\\/", $this->namSpcae, $modules);
        $m = isset($modules['m'])  ? $modules['m'] : "";
        $interface['modules'] = $m;
        //获取类注释
        $interface['name'] = $this->getStrRegular('name', $classDoc);
        $interface['desc'] = $this->getStrRegular('desc', $classDoc);
        $interface['sort'] = $this->getStrRegular('sort', $classDoc, 0);
        $interface['date'] = $this->getStrRegular('date', $classDoc);
        $interface['author'] = $this->getStrRegular('author', $classDoc);
        $interface['sign'] = $this->getStrRegular('sign', $classDoc, 0);
        $interface['alias'] = !empty($m) ? $m.'-'.$alias : $alias;
        //获取类的信息
        $interface['start_line'] = $reflections->getStartLine();
        $interface['end_line'] = $reflections->getEndLine();
        $interface['file_name'] = $reflections->getFileName();


        $interface['method'] = $this->parseMethod($reflections, !empty($m) ? $m.'/'.$alias : $alias);

        //设置缓存或缓存数据加密
        if (self::$cache) {

            !self::$encrypt ? \doc::$cache->set($chaceKey, $interface) : \doc::$cache->setEncrypt()->set($chaceKey, $interface);
        }

        return $interface;


    }

    /**
     * 获取【类】【接口】方法的具体信息
     * @param \ReflectionClass $reflection
     * @return array
     */
    private function parseMethod(\ReflectionClass $reflection, $router)
    {
        $methodsData = [];
        $means = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC);
        foreach ($means as $obj) {
            $doc = $obj->getDocComment();
            $method['title']=$obj->getName();
            $action = strtolower(substr($obj->getName(), strlen('action')));
            $route = !empty($router) ? rtrim($router, '/') . '/'. $action : $action;
            $alias = str_replace(['/', '_'],['-', '-'], $route);


            $method['md5'] = md5($obj->getName().$doc);
            $method['alias'] = $alias;
            $method['route'] = strpos($route, '_') !== false ? str_replace('_', '/', $route) :  $route;
            $method['name'] = $action;
            $method['class']=$obj->class;
            $method['start_line']=$obj->getStartLine();
            $method['end_line']=$obj->getEndLine();
            //获取注释
            $method['title'] = $this->getStrRegular('name', $doc);
            $method['sort'] = $this->getStrRegular('sort', $doc, 1);
            $method['remark'] = $this->getStrRegular('remark', $doc);
            $method['date'] = $this->getStrRegular('date', $doc);
            $method['deprecated'] = $this->getStrRegular('deprecated', $doc, 0);
            $method['post'] = $this->getStrMethodArgc('post', $doc);
            $method['get'] = $this->getStrMethodArgc('get', $doc);

            $case = $this->getStrMethodResponse('case', $doc);
            $newCase = $this->getStrMethodCases('case-', $doc);
            if ($newCase && !empty($newCase)) {
                if (!empty($case)) {
                    array_unshift($newCase, $case);
                }
            } else {
                $newCase = $case;
            }
            $method['case'] = $newCase;
            $method['sign'] = $this->getStrRegular('sign', $doc, 0);
            $method['post_remark'] = $this->getStrPostRemark('post-remark', $doc);
            $methodsData[$method['md5']] = $method;
        }
        return $methodsData;

    }

    /**
     * 正则表达式获取信息[类]
     * @param $reg
     * @param $str
     */
    private function getStrRegular($key, $str, $default='')
    {
        $regular = "/(?<{$key}>@{$key}.+)/";
        preg_match($regular, $str, $string);

        return isset($string[$key]) ? trim(ltrim($string[$key], "@{$key} ")) : $default;
    }

    /**
     * 获取方法说明
     * @param $key
     * @param $str
     * @param string $default
     * @return string
     */
    private function getStrMethod($key, $str, $default='')
    {
        $regular = "/(?<{$key}>@{$key}.+)/";
        preg_match($regular, $str, $argv);

        return isset($argv[$key]) ? trim(ltrim($argv[$key], "@{$key} ")) : $default;
    }

    /**
     * 获取post与给说明
     * @param $key
     * @param $str
     * @param string $default
     * @return array
     */
    private function getStrMethodArgc($key, $str, $default='')
    {
        $option = [];
        $regular = "/(?<{$key}>\@{$key} .+)/";
        preg_match_all($regular, $str, $argc);
        if (isset($argc[$key]) && is_array($argc[$key]) && !empty($argc[$key])) {

            foreach ($argc[$key] as $k=>$v) {
                $strStart =  trim(ltrim($v, "\@{$key} "));
                $strLine = explode(',',$strStart);


                $arr['not_null'] = false;
                foreach ($strLine as $item) {
                    list($i, $value) = explode(":", $item);
                    switch ($i) {
                        case 'k':
                            $arr['key']=$value;
                            break;
                        case 'v':
                            $arr['value']=$value;
                            break;
                        case 't':
                            $arr['title']=$value;
                            break;
                        case 'n':
                            $arr['not_null']=true;
                            break;
                    }
                }
                $option[]= $arr;
            }
        }

        return $option;
    }

    /**
     * 获取post的补充说明【json】
     * @param $key
     * @param $str
     * @param string $default
     * @return array
     */
    private function getStrPostRemark($key, $str, $default='')
    {
        $result = [];
        $k = strpos($key,'-') !== false ? str_replace('-', '_', $key) : $key;
        $regular = "/(?<{$k}>@{$key}.+)/";
        preg_match_all($regular, $str, $strArr);
        if (isset($strArr[$k]) && is_array($strArr[$k])) {
            foreach ($strArr[$k] as $v) {
                preg_match("/@{$key}-(?<k>[a-z_]{1,})/", $v, $item);
                $i = isset($item['k']) ? trim($item['k'])  : 'flag';
                $result[$i]  = trim(preg_replace("/@{$key}-[a-z_]{1,}/", '', $v));
            }
        }

        return $result;
    }

    /**
     * 获取返回信息【json】api
     * @param $key
     * @param $str
     * @param string $default
     * @return string
     */
    private function getStrMethodResponse($key, $str, $default='')
    {
        $regular = "/(?<{$key}>@{$key}.+)/";

        preg_match($regular, $str, $strArr);

        return isset($strArr[$key]) ? trim(ltrim($strArr[$key], "@{$key} ")) : $default;
    }

    /**
     * 获取返回信息【json】api
     * @param $key
     * @param $str
     * @param string $default
     * @return string
     */
    private function getStrMethodCases($key, $str, $default='')
    {
        //$regular = "/(?<{$key}>@{$key}.+)/";
        $regular = "/@{$key}[a-z_].+/";
        preg_match_all($regular, $str, $strArr);
        $list = [];
        if (isset($strArr[0]) && is_array($strArr[0])) {

            foreach ($strArr[0] as $item) {
                $item =  preg_replace("/[\s]+/is"," ",$item);
                $arr = explode(' ', $item);

                if (isset($arr[0]) && isset($arr[1])) {
                    $k = str_replace('_', "=", ltrim($arr[0], '@case-'));
                    $list[$k]= $arr[1];
                } else {
                    continue;
                }
            }
        }
        return $list;
    }

}